Domine o hook useFormState do React. Um guia completo para gerenciamento de estado de formulários, validação no servidor e experiência do usuário aprimorada com Server Actions.
React useFormState: Um Mergulho Profundo no Gerenciamento e Validação de Formulários Modernos
Formulários são a base da interatividade na web. De simples formulários de contato a assistentes complexos de várias etapas, eles são essenciais para a entrada do usuário e o envio de dados. Por anos, os desenvolvedores React navegaram por um cenário de soluções de gerenciamento de estado, desde simples hooks useState para cenários básicos até poderosas bibliotecas de terceiros como Formik e React Hook Form para necessidades mais complexas. Embora essas ferramentas sejam excelentes, o React está continuamente evoluindo para fornecer primitivas mais integradas e poderosas.
Apresentando o useFormState, um hook introduzido no React 18. Inicialmente projetado para funcionar perfeitamente com React Server Actions, o useFormState oferece uma abordagem nativa, robusta e simplificada para gerenciar o estado de formulários, especialmente ao lidar com lógica e validação do lado do servidor. Ele simplifica o processo de exibir feedback do servidor, como erros de validação ou mensagens de sucesso, diretamente na sua interface de usuário.
Este guia abrangente levará você a um mergulho profundo no hook useFormState. Exploraremos seus conceitos centrais, implementações práticas, padrões avançados e como ele se encaixa no ecossistema mais amplo do desenvolvimento React moderno. Esteja você construindo aplicações com Next.js, Remix ou React puro, entender o useFormState irá equipá-lo com uma ferramenta poderosa para construir formulários melhores e mais resilientes.
O que é `useFormState` e Por Que Precisamos Dele?
Em sua essência, o useFormState é um hook projetado para atualizar o estado com base no resultado de uma ação de formulário. Pense nele como uma versão especializada do useReducer, adaptada especificamente para submissões de formulário. Ele preenche elegantemente a lacuna entre a interação do usuário no lado do cliente e o processamento no lado do servidor.
Antes do useFormState, um fluxo típico de submissão de formulário envolvendo um servidor poderia ser assim:
- O usuário preenche um formulário.
- O estado do lado do cliente (por exemplo, usando
useState) rastreia os valores dos inputs. - Na submissão, um manipulador de eventos (
onSubmit) impede o comportamento padrão do navegador. - Uma requisição
fetché construída manualmente e enviada para um endpoint da API do servidor. - Os estados de carregamento são gerenciados (por exemplo,
const [isLoading, setIsLoading] = useState(false)). - O servidor processa a requisição, realiza a validação e interage com um banco de dados.
- O servidor envia de volta uma resposta JSON (por exemplo,
{ success: false, errors: { email: 'Invalid format' } }). - O código do lado do cliente analisa essa resposta e atualiza outra variável de estado para exibir erros ou mensagens de sucesso.
Este processo, embora funcional, envolve uma quantidade significativa de código repetitivo para gerenciar estados de carregamento, estados de erro e o ciclo de requisição/resposta. O useFormState, especialmente quando combinado com Server Actions, simplifica drasticamente isso ao criar um fluxo mais declarativo e integrado.
Os principais benefícios de usar o useFormState são:
- Integração Perfeita com o Servidor: É a solução nativa para lidar com respostas de Server Actions, tornando a validação do lado do servidor um cidadão de primeira classe em seu componente.
- Gerenciamento de Estado Simplificado: Centraliza a lógica para atualizações do estado do formulário, reduzindo a necessidade de múltiplos hooks
useStatepara dados, erros e status de submissão. - Aprimoramento Progressivo (Progressive Enhancement): Formulários construídos com
useFormStatee Server Actions podem funcionar mesmo que o JavaScript esteja desabilitado no cliente, pois são construídos sobre a base de submissões de formulário HTML padrão. - Experiência do Usuário Aprimorada: Facilita o fornecimento de feedback imediato e contextual ao usuário, como erros de validação inline ou mensagens de sucesso, logo após a submissão de um formulário.
Entendendo a Assinatura do Hook `useFormState`
Para dominar o hook, vamos primeiro analisar sua assinatura e valores de retorno. É mais simples do que parece à primeira vista.
const [state, formAction] = useFormState(action, initialState);
Parâmetros:
action: Esta é uma função que será executada quando o formulário for submetido. Esta função recebe dois argumentos: o estado anterior do formulário e os dados do formulário submetidos. Espera-se que ela retorne o novo estado. Geralmente, é uma Server Action, mas pode ser qualquer função.initialState: Este é o valor que você deseja que o estado do formulário tenha inicialmente, antes que qualquer submissão tenha ocorrido. Pode ser qualquer valor serializável (string, número, objeto, etc.).
Valores de Retorno:
useFormState retorna um array com exatamente dois elementos:
state: O estado atual do formulário. Na renderização inicial, este será oinitialStateque você forneceu. Após uma submissão de formulário, será o valor retornado pela sua funçãoaction. Este estado é o que você usa para renderizar feedback na UI, como mensagens de erro.formAction: Uma nova função de ação que você passa para a propactiondo seu elemento<form>. Quando esta ação é acionada (por uma submissão de formulário), o React chamará sua funçãoactionoriginal com o estado anterior e os dados do formulário, e então atualizará ostatecom o resultado.
Este padrão pode parecer familiar se você já usou o useReducer. A função action é como um reducer, o initialState é o estado inicial, e o React lida com o despacho para você quando o formulário é submetido.
Um Primeiro Exemplo Prático: Um Formulário de Inscrição Simples
Vamos construir um formulário simples de inscrição em newsletter para ver o useFormState em ação. Teremos um único input de e-mail e um botão de envio. A server action realizará uma validação básica para verificar se um e-mail foi fornecido e se está em um formato válido.
Primeiro, vamos definir nossa server action. Se você está usando Next.js, pode colocar isso no mesmo arquivo do seu componente adicionando a diretiva 'use server'; no topo da função.
// Em actions.js ou no topo do seu arquivo de componente com 'use server'
export async function subscribe(previousState, formData) {
const email = formData.get('email');
if (!email) {
return { message: 'E-mail é obrigatório.' };
}
// Uma regex simples para fins de demonstração
if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(email)) {
return { message: 'Por favor, insira um endereço de e-mail válido.' };
}
// Aqui você normalmente salvaria o e-mail em um banco de dados
console.log(`Inscrevendo-se com o e-mail: ${email}`);
// Simula um atraso
await new Promise(res => setTimeout(res, 1000));
return { message: 'Obrigado por se inscrever!' };
}
Agora, vamos criar o componente cliente que usa esta ação com o useFormState.
'use client';
import { useFormState } from 'react-dom';
import { subscribe } from './actions';
const initialState = {
message: null,
};
export function SubscriptionForm() {
const [state, formAction] = useFormState(subscribe, initialState);
return (
<form action={formAction}>
<h3>Inscreva-se na Nossa Newsletter</h3>
<div>
<label htmlFor="email">Endereço de E-mail</label>
<input type="email" id="email" name="email" required />
</div>
<button type="submit">Inscrever-se</button>
{state?.message && <p>{state.message}</p>}
</form>
);
}
Vamos analisar o que está acontecendo:
- Importamos
useFormStatedereact-dom(atenção: não dereact). - Definimos um objeto
initialState. Isso garante que nossa variávelstatetenha uma forma consistente desde a primeira renderização. - Chamamos
useFormState(subscribe, initialState). Isso vincula o estado do nosso componente à server actionsubscribe. - O
formActionretornado é passado para a propactiondo elemento<form>. Esta é a conexão mágica. - Renderizamos a mensagem do nosso objeto
statecondicionalmente. Na primeira renderização,state.messageénull, então nada é exibido. - Quando o usuário submete o formulário, o React invoca o
formAction. Isso aciona nossa server actionsubscribe. A funçãosubscriberecebe opreviousState(inicialmente, nossoinitialState) e oformData. - A server action executa sua lógica e retorna um novo objeto de estado (por exemplo,
{ message: 'E-mail é obrigatório.' }). - O React recebe este novo estado e renderiza novamente o componente
SubscriptionForm. A variávelstateagora contém o novo objeto, e nosso parágrafo condicional exibe a mensagem de erro ou sucesso.
Isso é incrivelmente poderoso. Implementamos um ciclo completo de validação cliente-servidor com o mínimo de código repetitivo para gerenciamento de estado no lado do cliente.
Aprimorando a UX com `useFormStatus`
Nosso formulário funciona, mas a experiência do usuário poderia ser melhor. Quando o usuário clica em "Inscrever-se", o botão permanece ativo e não há indicação visual de que algo está acontecendo até que o servidor responda. É aqui que entra o hook useFormStatus.
O hook useFormStatus fornece informações de status sobre a última submissão de formulário. É crucial que ele seja usado em um componente que seja filho do elemento <form>. Ele não funciona se for chamado no mesmo componente que renderiza o formulário.
Vamos criar um componente SubmitButton separado.
'use client';
import { useFormStatus } from 'react-dom';
export function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Inscrevendo...' : 'Inscrever-se'}
</button>
);
}
Agora, podemos atualizar nosso SubscriptionForm para usar este novo componente:
// ... imports
import { SubmitButton } from './SubmitButton';
// ... initialState e outro código
export function SubscriptionForm() {
const [state, formAction] = useFormState(subscribe, initialState);
return (
<form action={formAction}>
{/* ... inputs do formulário ... */}
<SubmitButton /> {/* Substitua o botão antigo */}
{state?.message && <p>{state.message}</p>}
</form>
);
}
Com esta mudança, quando o formulário é submetido, o valor pending de useFormStatus se torna true. Nosso componente SubmitButton é renderizado novamente, desabilitando o botão e mudando seu texto para "Inscrevendo...". Assim que a server action é concluída e o useFormState atualiza o estado, o formulário não está mais pendente e o botão retorna ao seu estado original. Isso fornece feedback essencial ao usuário e evita submissões duplicadas.
Validação Avançada com Estados de Erro Estruturados e Zod
Uma única string de mensagem é suficiente para formulários simples, mas aplicações do mundo real frequentemente exigem erros de validação por campo. Podemos facilmente alcançar isso retornando um objeto de estado mais estruturado de nossa server action.
Vamos aprimorar nossa action para retornar um objeto com uma chave errors, que por sua vez contém mensagens para campos específicos. Esta também é uma oportunidade perfeita para introduzir uma biblioteca de validação de esquema como Zod para uma lógica de validação mais robusta e de fácil manutenção.
Passo 1: Instalar o Zod
npm install zod
Passo 2: Atualizar a Server Action
Criaremos um esquema Zod para definir a forma esperada e as regras de validação para os dados do nosso formulário. Em seguida, usaremos schema.safeParse() para validar o formData recebido.
'use server';
import { z } from 'zod';
// Define o esquema para o nosso formulário
const contactSchema = z.object({
name: z.string().min(2, { message: 'O nome deve ter pelo menos 2 caracteres.' }),
email: z.string().email({ message: 'Endereço de e-mail inválido.' }),
message: z.string().min(10, { message: 'A mensagem deve ter pelo menos 10 caracteres.' }),
});
export async function submitContactForm(previousState, formData) {
const validatedFields = contactSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
});
// Se a validação falhar, retorne os erros
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'A validação falhou. Por favor, verifique seus dados.',
};
}
// Se a validação for bem-sucedida, processe os dados
// Por exemplo, envie um e-mail ou salve em um banco de dados
console.log('Sucesso!', validatedFields.data);
// ... lógica de processamento ...
// Retorne um estado de sucesso
return {
errors: {},
message: 'Obrigado pela sua mensagem! Entraremos em contato em breve.',
};
}
Observe como usamos validatedFields.error.flatten().fieldErrors. Esta é uma utilidade prática do Zod que transforma o objeto de erro em uma estrutura mais utilizável, como: { name: ['O nome deve ter pelo menos 2 caracteres.'], message: ['A mensagem é muito curta'] }.
Passo 3: Atualizar o Componente Cliente
Agora, vamos atualizar nosso componente de formulário para lidar com este estado de erro estruturado.
'use client';
import { useFormState } from 'react-dom';
import { submitContactForm } from './actions';
import { SubmitButton } from './SubmitButton'; // Supondo que temos um botão de envio
const initialState = {
message: null,
errors: {},
};
export function ContactForm() {
const [state, formAction] = useFormState(submitContactForm, initialState);
return (
<form action={formAction}>
<h2>Fale Conosco</h2>
<div>
<label htmlFor="name">Nome</label>
<input type="text" id="name" name="name" />
{state.errors?.name && (
<p className="error">{state.errors.name[0]}</p>
)}
</div>
<div>
<label htmlFor="email">Email</label>
<input type="email" id="email" name="email" />
{state.errors?.email && (
<p className="error">{state.errors.email[0]}</p>
)}
</div>
<div>
<label htmlFor="message">Mensagem</label>
<textarea id="message" name="message" />
{state.errors?.message && (
<p className="error">{state.errors.message[0]}</p>
)}
</div>
<SubmitButton />
{state.message && <p className="form-status">{state.message}</p>}
</form>
);
}
Este padrão é incrivelmente escalável e robusto. Sua server action se torna a única fonte da verdade para a lógica de validação, e o Zod fornece uma maneira declarativa e com segurança de tipos para definir essas regras. O componente cliente simplesmente se torna um consumidor do estado fornecido pelo useFormState, exibindo os erros onde eles pertencem. Essa separação de responsabilidades torna o código mais limpo, mais fácil de testar e mais seguro, já que a validação é sempre aplicada no servidor.
`useFormState` vs. Outras Soluções de Gerenciamento de Formulários
Com uma nova ferramenta, surge a pergunta: "Quando devo usar isso em vez do que já conheço?" Vamos comparar o useFormState com outras abordagens comuns.
`useFormState` vs. `useState`
- `useState` é perfeito para formulários simples, apenas no lado do cliente, ou quando você precisa realizar interações complexas em tempo real no lado do cliente (como validação ao vivo enquanto o usuário digita) antes da submissão. Ele oferece controle direto e granular.
- `useFormState` se destaca quando o estado do formulário é determinado principalmente por uma resposta do servidor. Ele é projetado para o ciclo de requisição/resposta da submissão de formulários e é a escolha ideal ao usar Server Actions. Ele elimina a necessidade de gerenciar manualmente chamadas fetch, estados de carregamento e análise de respostas.
`useFormState` vs. Bibliotecas de Terceiros (React Hook Form, Formik)
Bibliotecas como React Hook Form e Formik são soluções maduras e ricas em recursos que oferecem um conjunto abrangente de ferramentas para gerenciamento de formulários. Elas fornecem:
- Validação avançada no lado do cliente (frequentemente com integração de esquemas para Zod, Yup, etc.).
- Gerenciamento de estado complexo para campos aninhados, arrays de campos e mais.
- Otimizações de desempenho (por exemplo, isolando re-renderizações apenas para os inputs que mudam).
- Auxiliares para componentes controlados e integração com bibliotecas de UI.
Então, quando escolher cada um?
- Escolha
useFormStatequando:- Você está usando React Server Actions e deseja uma solução nativa e integrada.
- Sua principal fonte da verdade para validação é o servidor.
- Você valoriza o aprimoramento progressivo e quer que seus formulários funcionem sem JavaScript.
- A lógica do seu formulário é relativamente direta e centrada no ciclo de submissão/resposta.
- Escolha uma biblioteca de terceiros quando:
- Você precisa de validação extensa e complexa no lado do cliente com feedback imediato (por exemplo, validar no blur ou na mudança).
- Você tem formulários altamente dinâmicos (por exemplo, adicionar/remover campos, lógica condicional).
- Você não está usando um framework com Server Actions e está construindo sua própria camada de comunicação cliente-servidor com APIs REST ou GraphQL.
- Você precisa de controle refinado sobre o desempenho e as re-renderizações em formulários muito grandes.
Também é importante notar que eles não são mutuamente exclusivos. Você pode usar o React Hook Form para gerenciar o estado e a validação do seu formulário no lado do cliente e, em seguida, usar seu manipulador de submissão para chamar uma Server Action. No entanto, para muitos casos de uso comuns, a combinação de useFormState e Server Actions oferece uma solução mais simples e elegante.
Melhores Práticas e Armadilhas Comuns
Para tirar o máximo proveito do useFormState, considere as seguintes melhores práticas:
- Mantenha as Ações Focadas: Sua função de ação do formulário deve ser responsável por uma coisa: processar a submissão do formulário. Isso inclui validação, mutação de dados (salvar em um BD) e retornar o novo estado. Evite efeitos colaterais que não estejam relacionados ao resultado do formulário.
- Defina uma Forma de Estado Consistente: Sempre comece com um
initialStatebem definido e garanta que sua ação sempre retorne um objeto com a mesma forma, mesmo em caso de sucesso. Isso evita erros de tempo de execução no cliente ao tentar acessar propriedades comostate.errors. - Adote o Aprimoramento Progressivo: Lembre-se que Server Actions funcionam sem JavaScript no lado do cliente. Projete sua UI para lidar com ambos os cenários de forma elegante. Por exemplo, garanta que as mensagens de validação renderizadas no servidor sejam claras, pois o usuário não terá o benefício de um estado de botão desabilitado sem JS.
- Separe as Preocupações da UI: Use componentes como nosso
SubmitButtonpara encapsular a UI dependente de status. Isso mantém seu componente de formulário principal mais limpo e respeita a regra de queuseFormStatusdeve ser usado em um componente filho. - Não se Esqueça da Acessibilidade: Ao exibir erros, use atributos ARIA como
aria-invalidem seus campos de entrada e associe as mensagens de erro aos seus respectivos inputs usandoaria-describedbypara garantir que seus formulários sejam acessíveis a usuários de leitores de tela.
Armadilha Comum: Usar useFormStatus no Mesmo Componente
Um erro frequente é chamar useFormStatus no mesmo componente que renderiza a tag <form>. Isso não funcionará porque o hook precisa estar dentro do contexto do formulário para acessar seu status. Sempre extraia a parte da sua UI que precisa do status (como um botão) para seu próprio componente filho.
Conclusão
O hook useFormState, em conjunto com as Server Actions, representa uma evolução significativa na forma como lidamos com formulários no React. Ele incentiva os desenvolvedores a adotarem um modelo de validação mais robusto e centrado no servidor, ao mesmo tempo que simplifica o gerenciamento de estado no lado do cliente. Ao abstrair as complexidades do ciclo de vida da submissão, ele nos permite focar no que é mais importante: definir nossa lógica de negócios e construir uma experiência de usuário fluida.
Embora possa não substituir bibliotecas de terceiros abrangentes para todos os casos de uso, o useFormState fornece uma base poderosa, nativa e progressivamente aprimorada para a grande maioria dos formulários em aplicações web modernas. Ao dominar seus padrões e entender seu lugar no ecossistema React, você pode construir formulários mais resilientes, de fácil manutenção e amigáveis ao usuário com menos código e maior clareza.